AWS WAF コストの最適化、静的コンテンツの保護とログ記録を除外した利用を試してみた
AWS WAF Bot Control マネージドルール、リクエスト数に応じて追加の課金が発生します。
- Common USD 1.00/検査するリクエスト 100 万件
- Targeted USD 10.00/検査するリクエスト 100 万件
JPEG、PNG などの画像ファイルといった静的コンテンツと、 APIなどの動的コンテンツを 同一のCloudFrontで配信する環境で、静的コンテンツへのリクエストを Bot Control ルールとログ記録の対象外とすることで、AWS WAF コストの最適化を試みる機会がありましたので、紹介します
利用イメージ
黄緑色の線に示した、S3オリジンの静的コンテンツ(画像)へのリクエストを、BotControlルールと、WAFのログ記録の対象外としました。
WAF設定
CloudFront 用の WAF Web ACL とルールを、CloudFormation を利用して構築しました。
WAF設定
画像リクエストの判定
- 画像ファイルの拡張子を定義した正規表現セットを用意しました。
ExcludePathList:
Type: AWS::WAFv2::RegexPatternSet
Properties:
Name: !Sub '${AWS::StackName}-ExcludePathList'
Scope: CLOUDFRONT
Description: Paths to exclude from Bot Control
RegularExpressionList:
- ^.*\.png$
- ^.*\.svg$
- UriPath が 画像ファイルの拡張子と一致した場合、リクエストを許可 (Allow)。以降のルール (優先度:Priority 2以上)の評価は、省略する指定としました。
- 画像ファイルに一致したリクエストに対しラベルを付与しました。
Rules:
- Name: !Sub '${AWS::StackName}-ExcludePathList'
Priority: 1
Statement:
RegexPatternSetReferenceStatement:
FieldToMatch:
UriPath: {}
Arn: !GetAtt 'ExcludePathList.Arn'
TextTransformations:
- Type: LOWERCASE
Priority: 0
Action:
Allow: {}
VisibilityConfig:
CloudWatchMetricsEnabled: false
MetricName: !Sub '${AWS::StackName}-ExcludePathList'
SampledRequestsEnabled: false
RuleLabels:
- Name: !Sub '${AWS::StackName}-StaticAssetAllowList'
Botルール
- BotControlRule の保護対象定義した正規表現セットを用意しました。
BotControlPathList:
Type: AWS::WAFv2::RegexPatternSet
Properties:
Name: !Sub '${AWS::StackName}-BotControlPathList'
Scope: CLOUDFRONT
Description: Paths targeted for Bot Control
誤検知を避けるため、ブロックを行わない検出(Count)のみとする、OverrideActionを指定しました。
OverrideAction:
Count: {}
レートルール
Botルールで付与されたラベル情報を利用。
今回、CategoryAI に分類されたリクエストをレートルール対象とする設定を試みました。
Action:
Block: {}
Statement:
RateBasedStatement:
Limit: 10
AggregateKeyType: IP
ScopeDownStatement:
OrStatement:
Statements:
- LabelMatchStatement:
Scope: LABEL
Key: awswaf:managed:aws:bot-control:CategoryAI
- LabelMatchStatement:
Scope: LABEL
Key: awswaf:managed:aws:bot-control:bot:category:ai
VisibilityConfig:
CloudWatchMetricsEnabled: true
MetricName: !Sub '${AWS::StackName}-Ratebased1'
SampledRequestsEnabled: true
レートルールのしきい値、2024年11月時点で最小は10から設定可能です。
ログ設定
- WAFログをCloudWatchLogsに保存するように設定しました。
- 静的コンテンツに該当するラベルを含むリクエストは、ログ記録対象外としました。
LoggingFilter:
DefaultBehavior: KEEP
Filters:
- Behavior: DROP
Requirement: MEETS_ALL
Conditions:
- LabelNameCondition:
LabelName: !Sub 'awswaf:${AWS::AccountId}:webacl:${AWS::StackName}-webacl:${AWS::StackName}-StaticAssetAllowList'
テンプレート全文
AWSTemplateFormatVersion: '2010-09-09'
Description: CloudFront WAF WebACL with Bot Control for specific paths and static asset allowlist
Resources:
WebACL:
Type: AWS::WAFv2::WebACL
Properties:
Name: !Sub '${AWS::StackName}-webacl'
DefaultAction:
Allow: {}
Description: Bot Control for specific paths and static asset allowlist
Scope: CLOUDFRONT
VisibilityConfig:
CloudWatchMetricsEnabled: true
MetricName: !Sub '${AWS::StackName}'
SampledRequestsEnabled: false
Rules:
- Name: !Sub '${AWS::StackName}-ExcludePathList'
Priority: 1
Statement:
RegexPatternSetReferenceStatement:
FieldToMatch:
UriPath: {}
Arn: !GetAtt 'ExcludePathList.Arn'
TextTransformations:
- Type: LOWERCASE
Priority: 0
Action:
Allow: {}
VisibilityConfig:
CloudWatchMetricsEnabled: false
MetricName: !Sub '${AWS::StackName}-ExcludePathList'
SampledRequestsEnabled: false
RuleLabels:
- Name: !Sub '${AWS::StackName}-StaticAssetAllowList'
- Name: !Sub '${AWS::StackName}-AWSManagedRulesBotControlRuleSet'
Priority: 100
Statement:
ManagedRuleGroupStatement:
VendorName: AWS
Name: AWSManagedRulesBotControlRuleSet
Version: 'Version_3.0'
ScopeDownStatement:
RegexPatternSetReferenceStatement:
FieldToMatch:
UriPath: {}
Arn: !GetAtt 'BotControlPathList.Arn'
TextTransformations:
- Type: NONE
Priority: 0
ManagedRuleGroupConfigs:
- AWSManagedRulesBotControlRuleSet:
InspectionLevel: COMMON
#InspectionLevel: TARGETED
#EnableMachineLearning: true
OverrideAction:
Count: {}
VisibilityConfig:
CloudWatchMetricsEnabled: true
MetricName: !Sub '${AWS::StackName}-AWSManagedRulesBotControlRuleSet'
SampledRequestsEnabled: true
- Name: !Sub '${AWS::StackName}-Ratebased1'
Priority: 200
Action:
Block: {}
Statement:
RateBasedStatement:
Limit: 10
AggregateKeyType: IP
ScopeDownStatement:
OrStatement:
Statements:
- LabelMatchStatement:
Scope: LABEL
Key: awswaf:managed:aws:bot-control:CategoryAI
- LabelMatchStatement:
Scope: LABEL
Key: awswaf:managed:aws:bot-control:bot:category:ai
VisibilityConfig:
CloudWatchMetricsEnabled: true
MetricName: !Sub '${AWS::StackName}-Ratebased1'
SampledRequestsEnabled: true
ExcludePathList:
Type: AWS::WAFv2::RegexPatternSet
Properties:
Name: !Sub '${AWS::StackName}-ExcludePathList'
Scope: CLOUDFRONT
Description: Paths to exclude from Bot Control
RegularExpressionList:
- ^.*\.png$
- ^.*\.svg$
- ^.*\.ico$
- ^.*\.jpg$
- ^.*\.jpeg$
BotControlPathList:
Type: AWS::WAFv2::RegexPatternSet
Properties:
Name: !Sub '${AWS::StackName}-BotControlPathList'
Scope: CLOUDFRONT
Description: Paths targeted for Bot Control
RegularExpressionList:
- ^/articles/.*
- ^/$
WAFCloudWatchLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub 'aws-waf-logs-${AWS::StackName}'
RetentionInDays: 14
WAFLogConfig:
Type: AWS::WAFv2::LoggingConfiguration
Properties:
ResourceArn: !GetAtt WebACL.Arn
LogDestinationConfigs:
- !GetAtt WAFCloudWatchLogGroup.Arn
RedactedFields:
- SingleHeader:
Name: authorization
LoggingFilter:
DefaultBehavior: KEEP
Filters:
- Behavior: DROP
Requirement: MEETS_ALL
Conditions:
- LabelNameCondition:
LabelName: !Sub 'awswaf:${AWS::AccountId}:webacl:${AWS::StackName}-webacl:${AWS::StackName}-StaticAssetAllowList'
Not ScopeDownStatement
Bot ルールの定義で Not ScopeDownStatement を利用することで、特定パスを Bot ルールの除外設定とすることも可能です。ただし、Statement の階層が深くなることで設定ミスや管理の複雑化のリスクが高まるため、今回は除外設定のための WAF ルール費用 (月額 1 ドル) を容認しました。
Statement:
ManagedRuleGroupStatement:
VendorName: AWS
Name: AWSManagedRulesBotControlRuleSet
Version: 'Version_3.0'
ScopeDownStatement:
AndStatement:
Statements:
- NotStatement:
Statement:
RegexPatternSetReferenceStatement:
FieldToMatch:
UriPath: {}
Arn: !GetAtt 'ExcludePathList.Arn'
TextTransformations:
- Type: LOWERCASE
Priority: 0
- RegexPatternSetReferenceStatement:
FieldToMatch:
UriPath: {}
Arn: !GetAtt 'BotControlPathList.Arn'
TextTransformations:
- Type: NONE
Priority: 0
効果
Bot Conrolの対象を限定した事で、ルールの評価数を 毎分 1400件 → 400件、およそ30% の規模に抑制できました。
月間リクエスト数が 1000 万件発生するサイトの場合、Target Bot Control ルールの課金対象となるリクエスト数を 900 万件から 200 万件に抑制できます。これにより、Bot Control ルールの課金を 90 USD から 20 USD に削減できる可能性があります。
ログ確認
CloudWatch Logs に記録された WAF ログは、CloudWatch Logs Insight を利用して集計・分析できます。静的コンテンツのリクエストをログ記録対象から除外することで、Logs Insight のスキャン費用も抑制できます。
確認例
- クエリサンプル
fields @timestamp, @message
| parse @message /"action":"(?<action>[^"]+)"/
| parse @message /"country":"(?<country>[^"]+)"/
| parse @message /"clientIp":"(?<clientip>[^"]+)"/
| parse @message /(?i)"User-Agent","value":"(?<user_agent>[^"]+)"/
| parse @message /\{"timestamp":.*,"labels":(?<labels>.*?),"ja3Fingerprint"/
| filter labels like /awswaf:managed:aws:bot-control:bot/
| filter labels not like /awswaf:managed:aws:bot-control:bot:verified/
| filter labels not like /awswaf:managed:aws:bot-control:bot:name:slackbot/
| filter user_agent not like /Twitterbot/
| filter user_agent not like /Hatena/
| stats count(*) as request_count by action, country,clientip, user_agent, labels
| sort request_count desc
| limit 10000
-
fields @timestamp, @message
- ログエントリのタイムスタンプとメッセージ全体を選択
-
parse @message /"action":"(?<action>[^"]+)"/
- メッセージから "action" フィールドの値を抽出し、action という変数に格納
- 例: ALLOW, BLOCK, COUNT
-
parse @message /"country":"(?<country>[^"]+)"/
- メッセージから "country" フィールドの値を抽出し、country という変数に格納
- 例: US, JP, CN
-
parse @message /"clientIp":"(?<clientip>[^"]+)"/
- メッセージから "clientIp" フィールドを抽出
-
parse @message /(?i)"User-Agent","value":"(?<user_agent>[^"]+)"/
- メッセージから "User-Agent" の値を抽出し、user_agent という変数に格納
- (?i) は大文字小文字を区別しないマッチングを行うフラグ
-
parse @message /{"timestamp":.,"labels":(?<labels>.?),"ja3Fingerprint"/
- メッセージから "labels" フィールド全体を抽出し、labels という変数に格納
-
filter labels like /awswaf:managed:aws:bot-control:targeted:aggregate:coordinated_activity:/
- "coordinated_activity" に関連するボット制御ラベルを持つログエントリのみをフィルタリング
-
複数行の filter labels not like ...
- 認定Bot(verified)、制限対象外とするボットをラベルで除外
-
stats count(*) as request_count by action, country, clientip, user_agent, labels
- フィルタリングされたレコードを action, country, clientip, user_agent, labels でグループ化し、各グループの出現回数を request_count として計算
-
sort request_count desc
-
結果を request_count の降順でソート
以下は、Logs Insight で特定のクライアント IP からのアクセスが多いことを確認した例です。
フィールド | 値 |
---|---|
action | ALLOW |
clientip | 117.102.xx.xx |
country | JP |
labels | [{"name":"awswaf:managed:token:absent"},{"name":"awswaf:managed:aws:bot-control:bot:category:http_library"},{"name":"awswaf:managed:aws:bot-control:bot:unverified"},{"name":"awswaf:managed:aws:bot-control:targeted:aggregate:volumetric:ip:token_absent"},{"name":"awswaf:managed:aws:bot-control:bot:name:python"},{"name":"awswaf:managed:captcha:absent"},{"name":"awswaf:managed:aws:bot-control:CategoryHttpLibrary"},{"name":"awswaf:managed:aws:bot-control:bot:name:python_requests"},{"name":"awswaf:managed:aws:bot-control:signal:non_browser_user_agent"}] |
request_count | 392 |
user_agent | python-requests/2.32.3 |
このような分析結果に基づき、特定のラベルを持つリクエストをレートルールの対象にすることで、副作用を抑えたアクセスを制限することができます。